Hubspot certificate object#3604
Conversation
Switch certificate syncing from contact properties to dedicated HubSpot custom objects. Adds API helpers to upsert course-run and program certificate objects (unique_app_id handling, association helpers, and schema defs), Celery tasks to enqueue certificate syncs, a management command to create/inspect certificate schemas and print objectType/association IDs, and signal handlers to enqueue tasks on certificate creation. Removes certificate fields from the contact serializer and updates contact sync logic (skip_certificates is now a deprecated no-op). Adds settings for object types and association IDs and updates/extends tests accordingly.
OpenAPI ChangesShow/hide ## Changes for v0.yaml:Unexpected changes? Ensure your branch is up-to-date with |
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
…l/mitxonline into hubspot-certificate-object
for more information, see https://pre-commit.ci
…l/mitxonline into hubspot-certificate-object
for more information, see https://pre-commit.ci
dsubak
left a comment
There was a problem hiding this comment.
Looks mostly good - I left a few comments/questions, but nothing that should be considered blocking.
Regarding testing - has create_hubspot_certificate_schema been run in the sandbox? I was poking around to see if I could verify the new object structure, but wasn't able to find them. I'm configured against the sandbox, if you'd like me to try and validate the functionality against mitxonlinedev-2026
| try: | ||
| transaction.on_commit( | ||
| lambda: hubspot_tasks.sync_course_run_certificate_with_hubspot.delay( | ||
| instance.id | ||
| ) | ||
| ) | ||
| except Exception: # pylint: disable=broad-except | ||
| logger = logging.getLogger(__name__) | ||
| logger.exception("Error syncing HubSpot course run certificate") | ||
| # avoid blocking certificate save flow |
There was a problem hiding this comment.
Does this try/except make sense to retain anymore? Previously we were doing some actual work in the upsert_custom_properties method and then tasking out in sync_hubspot_user. If I'm reading this correctly, now we're only catching exceptions that could come out of Django when registering that callback?
| ): | ||
| """When a ProgramCertificate model is created.""" | ||
| _ = created | ||
| try: |
There was a problem hiding this comment.
Same question here w/r/t the try/except
| "user_email", | ||
| ], | ||
| "associatedObjects": ["CONTACT"], | ||
| "properties": [ |
There was a problem hiding this comment.
Dealer's choice as to whether or not you want to do this - do you think it'd be useful to pull out the common properties and reference them here to reduce some boilerplate? It looks like user_email, issue_date, unique_app_id and is_revoked are all defined the same way - we could define those as constants and reference them in both places if we think they're always gonna be consistent.
| """Create a v4 association between a certificate custom object and a contact. | ||
|
|
||
| Uses create_default() which auto-creates associations with the default type. | ||
| The association_type_id parameter is kept for backward compatibility but not used. |
There was a problem hiding this comment.
Apologies for the dumb question, but where is association_type_id needed for backwards compatibility? This is net new code right?
I see we're looking it up from settings or the API to pass into this method but I'm not clear on why we need it.
|
|
||
| wait_for_hubspot_rate_limit() | ||
| if existing_id: | ||
| result = hubspot_client.crm.objects.basic_api.update( |
There was a problem hiding this comment.
Maybe hubspot_certificate_result? Just to make it a bit more clear what this response corresponds to?
I was using one of the older test environments. I think you're good to run this command against that environment. |
Remove broad exception handling in certificate signal handlers and always schedule HubSpot sync tasks via transaction.on_commit. Consolidate certificate custom-object property definitions by introducing CERTIFICATE_COMMON_PROPERTIES and CERTIFICATE_IS_REVOKED_OPTIONS and reuse them in course_run and program certificate schemas to reduce duplication. Remove unused _get_cert_contact_association_type_id and update sync functions to use the resolved custom object type and hubspot result variable (hubspot_certificate_result) when creating/updating and associating certificates. Update tests to use SimpleNamespace-based certificate fixtures, mock _get_custom_object_type_id_by_name, and adjust assertions to match the new association and object-type usage.
for more information, see https://pre-commit.ci
…l/mitxonline into hubspot-certificate-object
for more information, see https://pre-commit.ci
|
@cp-at-mit Looks good in mitxonlinedev-2026!
|
annagav
left a comment
There was a problem hiding this comment.
I was able to verify the creation of certificate objects 👍

What are the relevant tickets?
https://github.com/mitodl/hq/issues/11345
Description (What does it do?)
Changes the way we store certificate information in Hubspot. Currently we store this information in a multi-checkbox property on the Contact object in Hubspot. This PR changes that so now we create a custom Hubspot object for certificates, create a new object for each user's certificate, and then link the certificate objects to the contact object in Hubspot.
How can this be tested?
Run the new management command to ensure that the customer contract object is created in the Hubspot account you're using (
docker compose exec web python manage.py create_hubspot_certificate_schema)You then can create a new course run or program certificate for your user through Django admin. Once you save the new certificate, you should see the certificate linked to your contact in Hubspot. To see them in Hubspot, you may need to go to "Data Management" -> "Data Model". There you should see the custom objects.